package com.dbflow5.processor.definition.provider;

import com.dbflow5.contentprovider.annotation.PathSegment;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.Adders;
import com.squareup.javapoet.*;

import javax.lang.model.element.Modifier;
import java.util.List;
import java.util.Map;

public class ContentProvider {
    public static void appendDefault(CodeBlock.Builder code) {
        code.beginControlFlow("default:")
                .addStatement("throw new $T($S + $L)",
                        ClassName.get(IllegalArgumentException.class), "Unknown URI", Constants.PARAM_URI)
                .endControlFlow();
    }

    public static class Constants {
        public static final String PARAM_CONTENT_VALUES = "values";
        public static final String PARAM_URI = "uri";
    }

    /**
     * Get any code needed to use path segments. This should be called before creating the statement that uses
     * [.getSelectionAndSelectionArgs].
     *
     * @param definition definition
     * @return CodeBlock
     */
    public static CodeBlock getSegmentsPreparation(ContentUriDefinition definition) {
        CodeBlock.Builder codeBuilder = CodeBlock.builder();
        if (definition.segments != null && definition.segments.length != 0) {
            codeBuilder.addStatement("$T segments = uri.getPathSegments()",
                    ParameterizedTypeName.get(List.class, String.class));
        }
        return codeBuilder.build();
    }

    /**
     * Get code which creates the `selection` and `selectionArgs` parameters separated by a comma.
     *
     * @param definition definition
     * @return CodeBlock
     */
    public static CodeBlock getSelectionAndSelectionArgs(ContentUriDefinition definition) {
        if (definition.segments == null || definition.segments.length == 0) {
            return CodeBlock.builder().add("selection, selectionArgs").build();
        } else {
            CodeBlock.Builder selectionBuilder = CodeBlock.builder().add("$T.concatenateWhere(selection, \"", com.dbflow5.processor.ClassNames.DATABASE_UTILS);
            CodeBlock.Builder selectionArgsBuilder = CodeBlock.builder().add("$T.appendSelectionArgs(selectionArgs, new $T[] {",
                    com.dbflow5.processor.ClassNames.DATABASE_UTILS, String.class);
            boolean isFirst = true;
            for (PathSegment segment : definition.segments) {
                if (!isFirst) {
                    selectionBuilder.add(" AND ");
                    selectionArgsBuilder.add(", ");
                }
                selectionBuilder.add("$L = ?", segment.column());
                selectionArgsBuilder.add("segments.get($L)", segment.segment());
                isFirst = false;
            }
            selectionBuilder.add("\")");
            selectionArgsBuilder.add("})");
            return CodeBlock.builder().add(selectionBuilder.build()).add(", ").add(selectionArgsBuilder.build()).build();
        }
    }

    /**
     * Description:
     *
     * @author Andrew Grosner (fuzz)
     */
    public static class DeleteMethod implements MethodDefinition {
        private static final String PARAM_URI = "uri";
        private static final String PARAM_SELECTION = "selection";
        private static final String PARAM_SELECTION_ARGS = "selectionArgs";

        private final ContentProviderDefinition contentProviderDefinition;
        private final ProcessorManager manager;

        public DeleteMethod(ContentProviderDefinition contentProviderDefinition, ProcessorManager manager) {
            this.contentProviderDefinition = contentProviderDefinition;
            this.manager = manager;
        }

        @Override
        public MethodSpec methodSpec() {
            CodeBlock.Builder code = CodeBlock.builder();

            code.beginControlFlow("switch(MATCHER.match($L))", PARAM_URI);
            contentProviderDefinition.endpointDefinitions.forEach(it -> {
                it.contentUriDefinitions.forEach(uriDefinition -> {
                    if (uriDefinition.deleteEnabled) {
                        code.beginControlFlow("case " + uriDefinition.name + ":");
                        code.add(getSegmentsPreparation(uriDefinition));
                        code.add("long count = $T.getDatabase($T.class).delete($S, ",
                                com.dbflow5.processor.ClassNames.FLOW_MANAGER, contentProviderDefinition.databaseTypeName,
                                it.tableName);
                        code.add(getSelectionAndSelectionArgs(uriDefinition));
                        code.add(");\n");

                        new NotifyMethod(it, uriDefinition, com.dbflow5.contentprovider.annotation.NotifyMethod.DELETE).addCode(code);

                        code.addStatement("return (int) count");

                        code.endControlFlow();

                    }
                });
            });

            appendDefault(code);
            code.endControlFlow();

            return MethodSpec.methodBuilder("delete")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addParameter(com.dbflow5.processor.ClassNames.URI, PARAM_URI)
                    .addParameter(ClassName.get(String.class), PARAM_SELECTION)
                    .addParameter(ArrayTypeName.of(String.class), PARAM_SELECTION_ARGS)
                    .addCode(code.build()).returns(TypeName.INT).build();
        }
    }

    /**
     * Description:
     */
    public static class InsertMethod implements MethodDefinition {
        private final ContentProviderDefinition contentProviderDefinition;
        private final boolean isBulk;

        public InsertMethod(ContentProviderDefinition contentProviderDefinition, boolean isBulk) {
            this.contentProviderDefinition = contentProviderDefinition;
            this.isBulk = isBulk;
        }

        @Override
        public MethodSpec methodSpec() {
            CodeBlock.Builder code = CodeBlock.builder();
            code.beginControlFlow("switch(MATCHER.match($L))", Constants.PARAM_URI);

            contentProviderDefinition.endpointDefinitions.forEach(tableEndpointDefinition -> {
                tableEndpointDefinition.contentUriDefinitions.forEach(uriDefinition -> {
                    if (uriDefinition.insertEnabled) {
                        code.beginControlFlow("case $L:", uriDefinition.name);
                        code.addStatement("$T adapter = $T.getModelAdapter($T.getTableClassForName($T.class, $S))",
                                com.dbflow5.processor.ClassNames.MODEL_ADAPTER, com.dbflow5.processor.ClassNames.FLOW_MANAGER, com.dbflow5.processor.ClassNames.FLOW_MANAGER,
                                contentProviderDefinition.databaseTypeName, tableEndpointDefinition.tableName);

                        code.add("final long id = FlowManager.getDatabase($T.class)",
                                contentProviderDefinition.databaseTypeName).add(
                                ".insertWithOnConflict($S, null, values, " +
                                        "$T.getSQLiteDatabaseAlgorithmInt(adapter.getInsertOnConflictAction()));\n", tableEndpointDefinition.tableName,
                                com.dbflow5.processor.ClassNames.CONFLICT_ACTION);

                        new NotifyMethod(tableEndpointDefinition, uriDefinition, com.dbflow5.contentprovider.annotation.NotifyMethod.INSERT).addCode(code);

                        if (!isBulk) {
                            code.addStatement("return $T.withAppendedId($L, id)", com.dbflow5.processor.ClassNames.CONTENT_URIS, Constants.PARAM_URI);
                        } else {
                            code.addStatement("return id > 0 ? 1 : 0");
                        }
                        code.endControlFlow();
                    }
                });
            });

            appendDefault(code);
            code.endControlFlow();
            return MethodSpec.methodBuilder(isBulk ? "bulkInsert" : "insert")
                    .addAnnotation(Override.class).addParameter(com.dbflow5.processor.ClassNames.URI, Constants.PARAM_URI)
                    .addParameter(com.dbflow5.processor.ClassNames.CONTENT_VALUES, Constants.PARAM_CONTENT_VALUES)
                    .addModifiers(isBulk ? Modifier.PROTECTED : Modifier.PUBLIC, Modifier.FINAL)
                    .addCode(code.build()).returns(isBulk ? TypeName.INT : com.dbflow5.processor.ClassNames.URI).build();
        }
    }

    /**
     * Description:
     */
   public static class NotifyMethod implements Adders.CodeAdder {
        private final TableEndpointDefinition tableEndpointDefinition;
        private final ContentUriDefinition uriDefinition;
        private final com.dbflow5.contentprovider.annotation.NotifyMethod notifyMethod;

        public NotifyMethod(TableEndpointDefinition tableEndpointDefinition, ContentUriDefinition uriDefinition, com.dbflow5.contentprovider.annotation.NotifyMethod notifyMethod) {
            this.tableEndpointDefinition = tableEndpointDefinition;
            this.uriDefinition = uriDefinition;
            this.notifyMethod = notifyMethod;
        }

        @Override
        public CodeBlock.Builder addCode(CodeBlock.Builder code) {
            boolean hasListener = false;
            Map<com.dbflow5.contentprovider.annotation.NotifyMethod, List<NotifyDefinition>> notifyDefinitionMap =
                    tableEndpointDefinition.notifyDefinitionPathMap.get(uriDefinition.path);
            if (notifyDefinitionMap != null) {
                List<NotifyDefinition> notifyDefinitionList = notifyDefinitionMap.get(notifyMethod);
                if (notifyDefinitionList != null) {
                    for (NotifyDefinition notifyDefinition : notifyDefinitionList) {
                        notifyDefinition.addCode(code);
                        hasListener = true;
                    }
                }
            }

            if (!hasListener) {
                boolean isUpdateDelete = notifyMethod == com.dbflow5.contentprovider.annotation.NotifyMethod.UPDATE || notifyMethod == com.dbflow5.contentprovider.annotation.NotifyMethod.DELETE;
                if (isUpdateDelete) {
                    code.beginControlFlow("if (count > 0)");
                }

                code.addStatement("DataAbilityHelper.creator(getContext()).notifyChange(uri)");

                if (isUpdateDelete) {
                    code.endControlFlow();
                }
            }
            return code;
        }

    }

    /**
     * Description:
     */
    public static class QueryMethod implements MethodDefinition {
        private ContentProviderDefinition contentProviderDefinition;
        private ProcessorManager manager;

        public QueryMethod(ContentProviderDefinition contentProviderDefinition, ProcessorManager manager) {
            this.contentProviderDefinition = contentProviderDefinition;
            this.manager = manager;
        }

        public MethodSpec methodSpec() {
            MethodSpec.Builder method = MethodSpec.methodBuilder("query")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addParameter(com.dbflow5.processor.ClassNames.URI, "uri")
                    .addParameter(ArrayTypeName.of(String.class), "projection")
                    .addParameter(ClassName.get(String.class), "selection")
                    .addParameter(ArrayTypeName.of(String.class), "selectionArgs")
                    .addParameter(ClassName.get(String.class), "sortOrder")
                    .returns(com.dbflow5.processor.ClassNames.CURSOR);

            method.addStatement("$L cursor = null", com.dbflow5.processor.ClassNames.CURSOR);
            method.beginControlFlow("switch($L.match(uri))", ContentProviderDefinition.URI_MATCHER);
            for (TableEndpointDefinition tableEndpointDefinition : contentProviderDefinition.endpointDefinitions) {
                tableEndpointDefinition.contentUriDefinitions
                        .stream().filter(it -> it.queryEnabled)
                        .forEach(definition -> {
                            method.beginControlFlow("case $L:", definition.name);
                            method.addCode(getSegmentsPreparation(definition));
                            method.addCode("cursor = $T.getDatabase($T.class).query($S, projection, ",
                                    com.dbflow5.processor.ClassNames.FLOW_MANAGER, contentProviderDefinition.databaseTypeName,
                                    tableEndpointDefinition.tableName);
                            method.addCode(getSelectionAndSelectionArgs(definition));
                            method.addCode(", null, null, sortOrder);\n");
                            method.addStatement("break");
                            method.endControlFlow();
                        });
            }
            method.endControlFlow();

            method.beginControlFlow("if (cursor != null)");
            method.addStatement("cursor.setNotificationUri(getContext().getContentResolver(), uri)");
            method.endControlFlow();
            method.addStatement("return cursor");

            return method.build();
        }
    }

    /**
     * Description:
     */
    public static class UpdateMethod implements MethodDefinition {
        private final ContentProviderDefinition contentProviderDefinition;
        private final ProcessorManager manager;

        public UpdateMethod(ContentProviderDefinition contentProviderDefinition, ProcessorManager manager) {
            this.contentProviderDefinition = contentProviderDefinition;
            this.manager = manager;
        }

        @Override
        public MethodSpec methodSpec() {
            MethodSpec.Builder method = MethodSpec.methodBuilder("update")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(com.dbflow5.processor.ClassNames.URI, Constants.PARAM_URI)
                    .addParameter(com.dbflow5.processor.ClassNames.CONTENT_VALUES, Constants.PARAM_CONTENT_VALUES)
                    .addParameter(ClassName.get(String.class), "selection")
                    .addParameter(ArrayTypeName.of(String.class), "selectionArgs")
                    .returns(TypeName.INT);

            method.beginControlFlow("switch(MATCHER.match($L))", Constants.PARAM_URI);
            for (TableEndpointDefinition tableEndpointDefinition : contentProviderDefinition.endpointDefinitions) {
                tableEndpointDefinition.contentUriDefinitions
                        .stream().filter(it -> it.updateEnabled)
                        .forEach(definition -> {
                            method.beginControlFlow("case $L:", definition.name);
                            method.addStatement("$T adapter = $T.getModelAdapter($T.getTableClassForName($T.class, $S))",
                                    com.dbflow5.processor.ClassNames.MODEL_ADAPTER, com.dbflow5.processor.ClassNames.FLOW_MANAGER, com.dbflow5.processor.ClassNames.FLOW_MANAGER,
                                    contentProviderDefinition.databaseTypeName,
                                    tableEndpointDefinition.tableName);
                            method.addCode(getSegmentsPreparation(definition));
                            method.addCode(
                                    "long count = $T.getDatabase($T.class).updateWithOnConflict($S, $L, ",
                                    com.dbflow5.processor.ClassNames.FLOW_MANAGER, contentProviderDefinition.databaseTypeName,
                                    tableEndpointDefinition.tableName,
                                    Constants.PARAM_CONTENT_VALUES);
                            method.addCode(getSelectionAndSelectionArgs(definition));
                            method.addCode(
                                    ", $T.getSQLiteDatabaseAlgorithmInt(adapter.getUpdateOnConflictAction()));\n",
                                    com.dbflow5.processor.ClassNames.CONFLICT_ACTION);

                            CodeBlock.Builder code = CodeBlock.builder();
                            new NotifyMethod(tableEndpointDefinition, definition,
                                    com.dbflow5.contentprovider.annotation.NotifyMethod.UPDATE).addCode(code);
                            method.addCode(code.build());

                            method.addStatement("return (int) count");
                            method.endControlFlow();
                        });
            }

            CodeBlock.Builder code = CodeBlock.builder();
            appendDefault(code);
            method.addCode(code.build());
            method.endControlFlow();

            return method.build();
        }
    }
}